(define-syntax and
  (syntax-rules ()
    ;; Trivial case of no arguments.
    [(and) #t]
    ;; If there is only one expression, the expansion is the value produced by that one expression.
    [(and exp) exp]
    ;; The case of multiple arguments. We destructure into the first expression and the rest of
    ;; expressions.
    [(and exp0 exps ...)
     ;; Check the first expression given to `and`.
     (if exp0
         ;; Recur: Expand with the remaining expressions.
         (and exps ...)
         ;; If only one expression is falsy, the whole expansion is false.
         #f)]))

(define-syntax or
  (syntax-rules ()
    [(or) #t]
    [(or exp) exp]
    [(or exp0 exps ...)
     (if exp0
         exp0
         (or exps ...))]))

;; However, we could also use a case-lambda to define `and` or `or`. Maybe it would be better to
;; avoid the macros in this case. (Although case-lambda might actually do the same behind the scenes
;; as our macros (?), it would still improve readability to use case-lambda.)
(define and
  (case-lambda
    [() #t]
    [(expr) expr]
    [(expr . rest-exprs)
     (if expr
         (apply and rest-exprs)
         #f)]))

(define or
  (case-lambda
    [() #t]
    [(expr) expr]
    [(expr . rest-exprs)
     (if expr
         expr
         (apply and rest-exprs))]))
